package edu.utexas.esel.jgraphpad.archpadplugin.ui;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.CellEditor;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.text.TextAction;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.jgraph.graph.DefaultGraphCell;

import com.jgraph.editor.JGraphEditorResources;

import edu.utexas.esel.jgraphpad.archpadplugin.ArchPadPlugin;
import edu.utexas.esel.jgraphpad.archpadplugin.model.Element;
import edu.utexas.esel.jgraphpad.archpadplugin.model.ElementTreeNode;
import edu.utexas.esel.jgraphpad.archpadplugin.model.Element.ElementDeclaration;

/**
 * This class is the editor that performs editing of {@link Element}s. It
 * wraps the real editor which does the editing is the form of a dialog.
 * This is required since the editor component used by a {@link CellEditor}
 * is added to the edited entity where it is rendered which does not allow
 * using full screen in dialog style.
 */
public class ElementEditor extends Component {

	private static final long serialVersionUID = 1L;

	private RealEditor mRealEditor = new RealEditor();

    private CellEditor mCellEditor;

    private Element mCurrentValue;

	/**
	 * Initializes the singleton instance of this class to collaborate with
	 * a given {@link CellEditor} to edit the value of an
	 * {@link Element}.
	 *
	 * @param cellEditor
	 * @param currentModel
	 */
	public void initialize(CellEditor cellEditor, Element element) {
	    mCellEditor = cellEditor;
	    mCurrentValue = element;
	    mRealEditor.bindModel(element);
	}

	@Override
	public void setVisible(boolean b) {
		mRealEditor.setVisible(true);
	}

    /**
     * Returns the current value of the edited cell.
     *
     * @return The value.
     */
    public Element getCurrentValue() {
		return mCurrentValue;
	}

	/**
	 * Class that performs the real editing in the form of a dialog.
	 */
    private class RealEditor extends JFrame implements TreeSelectionListener,
        ActionListener, TreeModelListener {

	    private static final long serialVersionUID = 1L;

	    private static final String NEW_ELEMENT = "newelement";

	    private static final String ADD = "add";

	    private static final String DELETE = "delete";

	    private static final String RATIONALE = "rationale";

	    private static final String ACTIONS = "actions";

	    private static final String ACCEPT_CANCEL_EDIT = "acceptcanceledit";

	    private static final String OK = "ok";

	    private static final String CANCEL = "cancel";

	    private static final String MODEL = "model";

	    private static final String KEY_ESC_ACTION = "ESC";

	    private JTree mTree;

	    private JEditorPane mRationaleEditor;

	    private JPanel mActionsPannel;

	    private ElementTreeNode mCurrentSelection;

	    private Element mEditedModel;

		/**
		 * Creates a new instance.
		 */
	    private RealEditor() {
	    	configure();
	        createUI();
	        bindKeyStrokeActions();
	    }

		/**
	     * Binds the editor to a {@link TreeModel} to be edited.
	     *
	     * @param treeModel The tree model.
	     */
		public void bindModel(Element model) {
			unbindModel();

			mEditedModel = model.clone();
	    	mEditedModel.addTreeModelListener(this);

			mTree.setModel(mEditedModel);
	    	DefaultMutableTreeNode root =
	    		(DefaultMutableTreeNode) mEditedModel.getRoot();
	    	mTree.setSelectionPath(new TreePath(root.getPath()));
	    	mTree.invalidate();

	    	for (int i = 0, count = mTree.getRowCount(); i < count; i++) {
	    		mTree.expandRow(i);
	    	}
	    }
	
		/**
	     * Unbinds the editor from a {@link TreeModel} that was edited.
	     */
		public void unbindModel() {
			if (mEditedModel != null) {
				mEditedModel.removeTreeModelListener(this);
				mEditedModel = null;
			}
		}
	
		/**
		 * Configures the editor.
		 */
	    private void configure() {
	        setLayout(new GridLayout(1, 1));
			setUndecorated(true);
	        setExtendedState(Frame.MAXIMIZED_BOTH);
	        setAlwaysOnTop(true);

	        // On close we want to stop editing
	    	setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
	        addWindowListener(new WindowAdapter() {
	
				@Override
				public void windowClosing(WindowEvent event) {
					mCellEditor.stopCellEditing();
				
				}
	    	});
	    }

	    /**
	     * Creates the editor UI.
	     */
	    private void createUI() {
	    	final JSplitPane splitPane = new JSplitPane(
	    			JSplitPane.HORIZONTAL_SPLIT, createLeftPanel(),
	    			createRightPanel());
	
	    	// Add a listener to set the divider location appropriately
	    	addComponentListener(new ComponentAdapter() {
				@Override
				public void componentResized(ComponentEvent event) {
					splitPane.setDividerLocation(0.5d);
				}
	    	});
	
	 	    splitPane.setOneTouchExpandable(true);
	        add(splitPane);
	    }

	    /**
	     * Binds key actions of the editor. 
	     */
		private void bindKeyStrokeActions() {
			JRootPane rootPane = getRootPane();

			KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
	    	rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
	    	    .put(keyStroke, KEY_ESC_ACTION);
	    	rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
		        .put(keyStroke, KEY_ESC_ACTION);

	    	rootPane.getActionMap().put(KEY_ESC_ACTION, new AbstractAction() {
	    		private static final long serialVersionUID = 1L;

				public void actionPerformed(ActionEvent e) {
					mCellEditor.stopCellEditing();
				}
			});
		}

		/**
		 * Creates the left UI panel.
		 *
		 * @return The left panel.
		 */
		private Component createLeftPanel() {
			return createTreeScrollPane();
		}

		/**
		 * Creates the right UI panel.
		 *
		 * @return The panel.
		 */
		private Component createRightPanel() {
				JPanel rightPanel = new JPanel();
			rightPanel.setMinimumSize(new Dimension(200, 200));
				rightPanel.setLayout(new GridBagLayout());

			GridBagConstraints constraints = new GridBagConstraints();

		   	constraints.fill = GridBagConstraints.BOTH;
			constraints.gridwidth = GridBagConstraints.REMAINDER;
	    	constraints.weightx = 0.5;
	    	constraints.weighty = 1.0;
	    	rightPanel.add(createEditorScrollPane(), constraints);

	    	constraints.weightx = 0.5;
	    	constraints.weighty = 0.0;
	    	mActionsPannel = createAcionControlsPanel();
	    	rightPanel.add(mActionsPannel, constraints);

	    	constraints.weightx = 0.5;
	    	constraints.weighty = 0.0;
	    	constraints.gridheight = GridBagConstraints.REMAINDER;
	    	rightPanel.add(createAcceptCancelEditPanel(), constraints);

	    	return rightPanel;
	    }
	
		/**
		 * Creates a scroll pane for fitting the rationale editor.
		 *
		 * @return The scroll pane.
		 */
	    private JScrollPane createEditorScrollPane() {
	    	mRationaleEditor = new JEditorPane();
	        JScrollPane commentScrollPane = new JScrollPane(mRationaleEditor);
	        commentScrollPane.setVerticalScrollBarPolicy(
	        	JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
	        setTitledBorder(commentScrollPane,
	        	JGraphEditorResources.getString(RATIONALE));
	        return commentScrollPane;
	    }

	    /**
	     * Creates the panel with buttons for available actions in
	     * the given context.
	     *
	     * @return The panel.
	     */
		private JPanel createAcionControlsPanel() {
	        JPanel actionControlsPanel = new JPanel();
	        setTitledBorder(actionControlsPanel,
	        	JGraphEditorResources.getString(ACTIONS));

	        GridLayout layoutManager = new GridLayout(0, 2);
	        layoutManager.setHgap(5);
	        layoutManager.setVgap(5);
	        actionControlsPanel.setLayout(layoutManager);

	        return actionControlsPanel;
	    } 

	    /**
	     * Creates the panel with buttons for accepting or canceling edits.
	     *
	     * @return The panel.
	     */
		private JPanel createAcceptCancelEditPanel() {
	        JPanel acceptCancelEditPanel = new JPanel();
	        setTitledBorder(acceptCancelEditPanel,
	        	JGraphEditorResources.getString(ACCEPT_CANCEL_EDIT));

	        GridLayout layoutManager = new GridLayout(1, 2);
	        layoutManager.setHgap(5);
	        layoutManager.setVgap(5);
	        acceptCancelEditPanel.setLayout(layoutManager);

	        JButton okButton =
	        	new JButton(JGraphEditorResources.getString(OK));
	        okButton.setActionCommand(OK);
	        okButton.addActionListener(this);
	        acceptCancelEditPanel.add(okButton);

	        JButton cancelButton =
	        	new JButton(JGraphEditorResources.getString(CANCEL));
	        cancelButton.setActionCommand(CANCEL);
	        cancelButton.addActionListener(this);
	        acceptCancelEditPanel.add(cancelButton);

	        return acceptCancelEditPanel;
	    }

		@Override
		public void actionPerformed(ActionEvent event) {
			String command = event.getActionCommand();
			if (OK.equals(command)) {
				DefaultGraphCell container = mCurrentValue.getContainerCell();
				mEditedModel.setContainerCell(container);
				mCurrentValue = mEditedModel;
			}

			mCellEditor.stopCellEditing();
			unbindModel();
			setVisible(false);
		}

		/**
		 * Creates a scroll pane that contains the {@link JTree} that
		 * visualizes the Software Architecture tree model.
		 *
		 * @return The scroll pane.
		 */
	    private JScrollPane createTreeScrollPane() {
	    	// put a dummy model by to prevent the tree to use the default one
	    	ElementTreeNode dummyNode =
	    		new ElementTreeNode(ArchPadPlugin.TYPE_DECLARATION_COMPONENT);
	    	TreeModel dummyModel = new Element(dummyNode);
	    	mTree = new JTree(dummyModel);
	        mTree.setEditable(true);
	        mTree.setCellEditor(new TreeCellEditor(mTree));
	        mTree.setCellRenderer(new TreeCellRenderer());
	        mTree.addTreeSelectionListener(this);
	        mTree.getSelectionModel().setSelectionMode(
			    TreeSelectionModel.SINGLE_TREE_SELECTION);

	        JScrollPane treeScrollPane = new JScrollPane(mTree);
	    	treeScrollPane.setMinimumSize(new Dimension(200, 200));
	  		setTitledBorder(treeScrollPane,
	  			JGraphEditorResources.getString(MODEL));

	        return treeScrollPane;
	    }

	    /**
	     * Sets a title border for a {@link Component} given the title.
	     *
	     * @param jComponent The component.
	     * @param title The title.
	     */
	    private void setTitledBorder(JComponent jComponent, String title) {
		    jComponent.setBorder(BorderFactory.createCompoundBorder(
	            BorderFactory.createTitledBorder(title),
	            BorderFactory.createEmptyBorder(5, 5, 5, 5)));
	    }

	    /**
	     * Callback for changing the selection in the tree that represents
	     * the Software Architecture model. Upon selection change buttons
	     * of possible actions in the given context are dynamically generated.
	     */
	    public void valueChanged(TreeSelectionEvent event) {        	
	    	if (mCurrentSelection != null) {
	    		String text = mRationaleEditor.getText().trim();
	    		if (text != null && !text.isEmpty()) {
	    		    mCurrentSelection.setAttribute(
	    			    	ElementTreeNode.ATTRIBUTE_RATIONALE,
	    				    mRationaleEditor.getText());
	    		}
	    	}

	    	mRationaleEditor.setText("");
	    	mCurrentSelection =
	    		(ElementTreeNode) event.getPath().getLastPathComponent();

	    	String rationale = mCurrentSelection.getAttribute(
	    			ElementTreeNode.ATTRIBUTE_RATIONALE);

	    	if (rationale != null && !rationale.isEmpty()) { 
	    	    mRationaleEditor.setText(rationale);
	    	}

	    	addActionButtons(mCurrentSelection);

	    	boolean isVisible = (mActionsPannel.getComponents().length > 0);
	        mActionsPannel.setVisible(isVisible);
	    }

	    /**
	     * Create the buttons for available actions in the current context.
	     *
	     * @param selectedNode The current selection.
	     */
	    private void addActionButtons(ElementTreeNode node) {
	    	mActionsPannel.removeAll();

	    	List<ElementDeclaration> childDeclarations =
	    		Element.getChildrenTypeDeclarations(node);
	    	if (childDeclarations != null) {
	    	    for (ElementDeclaration decl : childDeclarations) {
	    	    	int count = mEditedModel.getChildrenOfNodeNameCount(node,
	    	    			decl.getName());
	    	    	if (decl.getMaxOccurs() < 0
	    	    			|| count < decl.getMaxOccurs()) {
	    	    		mActionsPannel.add(createAddButton(decl));
	    	    	}
	    	    }
	    	}

	    	// one can not delete the root
	    	if (node.getParent() != null) {
	    	    mActionsPannel.add(createDeleteButton());
	    	}

	    	mActionsPannel.invalidate();
	    	validate();
	    }

	    /**
	     * Creates a button for deletion the selected tree node.
	     *
	     * @return The button.
	     */
	    private JButton createDeleteButton() {
	    	String title = JGraphEditorResources.getString(DELETE);
	    	JButton button = new JButton(title);

	    	button.addActionListener(new ActionListener() {
	    		public void actionPerformed(ActionEvent event) {
	    			DefaultTreeModel model =
	    				(DefaultTreeModel) mTree.getModel();
	    			model.removeNodeFromParent(mCurrentSelection);
	    		}
	    	});
	    	return button;
	    }

	    /**
	     * Creates a button for adding a child with the given
	     * {@link ElementDeclaration}.
	     *
	     * @param declaration The type declaration for which to create an
	     *        add button
	     * @return The button.
	     */
	    private JButton createAddButton(final ElementDeclaration declaration) {
	    	String title = JGraphEditorResources.getString(ADD) + " " + 
		        JGraphEditorResources.getString(declaration.getName());

	        JButton button = new JButton(title);
	        button.setActionCommand(NEW_ELEMENT);

	        button.addActionListener(new ActionListener() {
	        	public void actionPerformed(ActionEvent event) {
	        		ElementTreeNode child = new ElementTreeNode(declaration);
	        	    mEditedModel.insertNodeInto(child, mCurrentSelection,
	        	    		mCurrentSelection.getChildCount());
	                mTree.scrollPathToVisible(new TreePath(child.getPath()));

	                // Remove the button if it is no longer required 
	                int count = mEditedModel.getChildrenOfNodeNameCount(
	                		mCurrentSelection, declaration.getName());
	                int max = mCurrentSelection.getDeclaration().getMaxOccurs();
	                if  (count == max) {
	                    Component src = (Component) event.getSource();
	                	mActionsPannel.remove(src);
	                	validate();
	                }

	                // For root we might have no buttons => hide the panel
	                if (mActionsPannel.getComponentCount() == 0) {
	                	mActionsPannel.setVisible(false);
	                }
	        	}
	        });

	        return button;
	    }

	    @Override
		public void treeNodesChanged(TreeModelEvent event) {
			/* do nothing */					
		}

	    @Override
		public void treeNodesInserted(TreeModelEvent event) {
	    	mTree.expandPath(event.getTreePath());
    	}

		@Override
    	public void treeNodesRemoved(TreeModelEvent e) {
		    /* do nothing */
		}

		@Override
		public void treeStructureChanged(TreeModelEvent e) {
			/* do nothing */
		}
	
	    /**
	     * This class is an editor for the elements in the tree in the lhs
	     * of the {@link ElementEditor}.
	     */
	    private class TreeCellEditor extends DefaultTreeCellEditor {

	    	private JTextField mEditorComponent;
	        private ElementTreeNode mSelection;

	    	TreeCellEditor(JTree tree) {
	    	super(tree, new DefaultTreeCellRenderer());

	    	addCellEditorListener(new CellEditorListener() {
				@Override
	    		public void editingCanceled(ChangeEvent e) {
					populateNewSelectionValue();
				}

				@Override
				public void editingStopped(ChangeEvent e) {
					populateNewSelectionValue();
				}			
			});

	    	mEditorComponent = new JTextField(50);
	    	mEditorComponent.getKeymap().addActionForKeyStroke(
	    		KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
	    		new TextAction("stop-edit-on-enter-press-action") {

	    			private static final long serialVersionUID = 1L;

	    			@Override
	    			public void actionPerformed(ActionEvent event) {
	                    stopCellEditing();
					}
	    		});
	    	}

			@Override
			public Component getTreeCellEditorComponent(JTree tree,
					Object node, boolean isSelected, boolean expanded,
					boolean leaf, int row) {
				mSelection = (ElementTreeNode) node;
				Object value = mSelection.getValue();
				String text = value != null ? value.toString() : "";
				mEditorComponent.setText(text);
				return mEditorComponent;
			}

			/**
			 * Populates the selection with the value of the editor.
			 */
			private void populateNewSelectionValue()  {
				String text = mEditorComponent.getText();
				text = !text.isEmpty() ? text : mSelection.toString();
				mSelection.setValue(text);
				mSelection = null;
			}
	    }

	    /**
	     * Renderer for showing type specific icons in the model tree.
	     */
	    private class TreeCellRenderer extends DefaultTreeCellRenderer {

	    	private static final long serialVersionUID = 1L;

	    	@Override
	        public Component getTreeCellRendererComponent(JTree tree,
	        		Object value, boolean selected, boolean expanded,
	        		boolean leaf, int row, boolean hasFocus) {

	        	super.getTreeCellRendererComponent(tree, value, selected,
	                expanded, leaf, row, hasFocus);

	        	Icon icon = null;
	        	ElementTreeNode node = (ElementTreeNode) value;
	        	String key = node.getDeclaration().getName() + ".icon";
	        	String uri = JGraphEditorResources.getString(key);
	        	if (uri != null) {
	        		icon = JGraphEditorResources.getImage(uri);
	        	}
        	    if (icon != null) {
        		    setIcon(icon);
        	    }

        	    return this;
            }
        }  
    }
}
